/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Serializable;
import java.net.URL;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.ElementHandler;

import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLFilter;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * <p> <code>SAXReader</code> creates a DOM4J tree from SAX parsing events.
 * </p>
 * 
 * <p> The actual SAX parser that is used by this class is configurable so you
 * can use your favourite SAX parser if you wish. DOM4J comes configured with
 * its own SAX parser so you do not need to worry about configuring the SAX
 * parser. </p>
 * 
 * <p> To explicitly configure the SAX parser that is used via Java code you can
 * use a constructor or use the {@link #setXMLReader(XMLReader)}or {@link
 * #setXMLReaderClassName(String)} methods. </p>
 * 
 * <p> If the parser is not specified explicitly then the standard SAX policy of
 * using the <code>org.xml.sax.driver</code> system property is used to
 * determine the implementation class of {@link XMLReader}. </p>
 * 
 * <p> If the <code>org.xml.sax.driver</code> system property is not defined
 * then JAXP is used via reflection (so that DOM4J is not explicitly dependent
 * on the JAXP classes) to load the JAXP configured SAXParser. If there is any
 * error creating a JAXP SAXParser an informational message is output and then
 * the default (Aelfred) SAX parser is used instead. </p>
 * 
 * <p> If you are trying to use JAXP to explicitly set your SAX parser and are
 * experiencing problems, you can turn on verbose error reporting by defining
 * the system property <code>org.dom4j.verbose</code> to be "true" which will
 * output a more detailed description of why JAXP could not find a SAX parser
 * </p>
 * 
 * <p> For more information on JAXP please go to <a
 * href="http://java.sun.com/xml/">Sun's Java &amp; XML site </a> </p>
 * 
 * @author <a href="mailto:james.strachan@metastuff.com">James Strachan </a>
 * @version $Revision: 1.2 $
 */
public class SAXReader
{
   private static final String SAX_STRING_INTERNING =
      "http://xml.org/sax/features/string-interning";
   private static final String SAX_NAMESPACE_PREFIXES =
      "http://xml.org/sax/features/namespace-prefixes";
   private static final String SAX_NAMESPACES =
      "http://xml.org/sax/features/namespaces";
   private static final String SAX_DECL_HANDLER =
      "http://xml.org/sax/properties/declaration-handler";
   private static final String SAX_LEXICAL_HANDLER =
      "http://xml.org/sax/properties/lexical-handler";
   private static final String SAX_LEXICALHANDLER =
      "http://xml.org/sax/handlers/LexicalHandler";

   /** <code>DocumentFactory</code> used to create new document objects */
   private DocumentFactory factory;

   /** <code>XMLReader</code> used to parse the SAX events */
   private XMLReader xmlReader;

   /** Whether validation should occur */
   private boolean validating;

   /** DispatchHandler to call when each <code>Element</code> is encountered */
   private DispatchHandler dispatchHandler;

   /** ErrorHandler class to use */
   private ErrorHandler errorHandler;

   /** The entity resolver */
   private EntityResolver entityResolver;

   /** Should element & attribute names and namespace URIs be interned? */
   private boolean stringInternEnabled = true;

   /** Should internal DTD declarations be expanded into a List in the DTD */
   private boolean includeInternalDTDDeclarations = false;

   /** Should external DTD declarations be expanded into a List in the DTD */
   private boolean includeExternalDTDDeclarations = false;

   /** Whether adjacent text nodes should be merged */
   private boolean mergeAdjacentText = false;

   /** Holds value of property stripWhitespaceText. */
   private boolean stripWhitespaceText = false;

   /** Should we ignore comments */
   private boolean ignoreComments = false;

   /** Encoding of InputSource - null means system default encoding */
   private String encoding = null;

   // private boolean includeExternalGeneralEntities = false;
   // private boolean includeExternalParameterEntities = false;

   /** The SAX filter used to filter SAX events */
   private XMLFilter xmlFilter;

   public SAXReader()
   {
   }

   public SAXReader(boolean validating)
   {
      this.validating = validating;
   }

   public SAXReader(DocumentFactory factory)
   {
      this.factory = factory;
   }

   public SAXReader(DocumentFactory factory, boolean validating)
   {
      this.factory = factory;
      this.validating = validating;
   }

   public SAXReader(XMLReader xmlReader)
   {
      this.xmlReader = xmlReader;
   }

   public SAXReader(XMLReader xmlReader, boolean validating)
   {
      this.xmlReader = xmlReader;
      this.validating = validating;
   }

   public SAXReader(String xmlReaderClassName) throws SAXException
   {
      if(xmlReaderClassName != null)
      {
         this.xmlReader = XMLReaderFactory.createXMLReader(xmlReaderClassName);
      }
   }

   public SAXReader(String xmlReaderClassName, boolean validating)
      throws SAXException
   {
      if(xmlReaderClassName != null)
      {
         this.xmlReader = XMLReaderFactory.createXMLReader(xmlReaderClassName);
      }

      this.validating = validating;
   }

   /**
    * Allows a SAX property to be set on the underlying SAX parser. This can be
    * useful to set parser-specific properties such as the location of schema or
    * DTD resources. Though use this method with caution as it has the
    * possibility of breaking the standard behaviour. An alternative to calling
    * this method is to correctly configure an XMLReader object instance and
    * call the {@link #setXMLReader(XMLReader)}method
    * 
    * @param name is the SAX property name
    * @param value is the value of the SAX property
    * 
    * @throws SAXException if the XMLReader could not be created or the property
    *            could not be changed.
    */
   public void setProperty(String name, Object value) throws SAXException
   {
      getXMLReader().setProperty(name, value);
   }

   /**
    * Sets a SAX feature on the underlying SAX parser. This can be useful to set
    * parser-specific features. Though use this method with caution as it has
    * the possibility of breaking the standard behaviour. An alternative to
    * calling this method is to correctly configure an XMLReader object instance
    * and call the {@link #setXMLReader(XMLReader)}method
    * 
    * @param name is the SAX feature name
    * @param value is the value of the SAX feature
    * 
    * @throws SAXException if the XMLReader could not be created or the feature
    *            could not be changed.
    */
   public void setFeature(String name, boolean value) throws SAXException
   {
      getXMLReader().setFeature(name, value);
   }

   /**
    * <p> Reads a Document from the given <code>File</code> </p>
    * 
    * @param file is the <code>File</code> to read from.
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(File file) throws DocumentException
   {
      try
      {
         /*
          * We cannot convert the file to an URL because if the filename
          * contains '#' characters, there will be problems with the URL in the
          * InputSource (because a URL like http://myhost.com/index#anchor is
          * treated the same as http://myhost.com/index) Thanks to Christian
          * Oetterli
          */
         InputSource source = new InputSource(new FileInputStream(file));
         if(this.encoding != null)
         {
            source.setEncoding(this.encoding);
         }
         String path = file.getAbsolutePath();

         if(path != null)
         {
            // Code taken from Ant FileUtils
            StringBuffer sb = new StringBuffer("file://");

            // add an extra slash for filesystems with drive-specifiers
            if(!path.startsWith(File.separator))
            {
               sb.append("/");
            }

            path = path.replace('\\', '/');
            sb.append(path);

            source.setSystemId(sb.toString());
         }

         return read(source);
      }
      catch(FileNotFoundException e)
      {
         throw new DocumentException(e.getMessage(), e);
      }
   }

   /**
    * <p> Reads a Document from the given <code>URL</code> using SAX </p>
    * 
    * @param url <code>URL</code> to read from.
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(URL url) throws DocumentException
   {
      String systemID = url.toExternalForm();

      InputSource source = new InputSource(systemID);
      if(this.encoding != null)
      {
         source.setEncoding(this.encoding);
      }

      return read(source);
   }

   /**
    * <p> Reads a Document from the given URL or filename using SAX. </p>
    * 
    * <p> If the systemId contains a <code>':'</code> character then it is
    * assumed to be a URL otherwise its assumed to be a file name. If you want
    * finer grained control over this mechansim then please explicitly pass in
    * either a {@link URL}or a {@link File}instance instead of a {@link
    * String} to denote the source of the document. </p>
    * 
    * @param systemId is a URL for a document or a file name.
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(String systemId) throws DocumentException
   {
      InputSource source = new InputSource(systemId);
      if(this.encoding != null)
      {
         source.setEncoding(this.encoding);
      }

      return read(source);
   }

   /**
    * <p> Reads a Document from the given stream using SAX </p>
    * 
    * @param in <code>InputStream</code> to read from.
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(InputStream in) throws DocumentException
   {
      InputSource source = new InputSource(in);
      if(this.encoding != null)
      {
         source.setEncoding(this.encoding);
      }

      return read(source);
   }

   /**
    * <p> Reads a Document from the given <code>Reader</code> using SAX </p>
    * 
    * @param reader is the reader for the input
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(Reader reader) throws DocumentException
   {
      InputSource source = new InputSource(reader);
      if(this.encoding != null)
      {
         source.setEncoding(this.encoding);
      }

      return read(source);
   }

   /**
    * <p> Reads a Document from the given stream using SAX </p>
    * 
    * @param in <code>InputStream</code> to read from.
    * @param systemId is the URI for the input
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(InputStream in, String systemId)
      throws DocumentException
   {
      InputSource source = new InputSource(in);
      source.setSystemId(systemId);
      if(this.encoding != null)
      {
         source.setEncoding(this.encoding);
      }

      return read(source);
   }

   /**
    * <p> Reads a Document from the given <code>Reader</code> using SAX </p>
    * 
    * @param reader is the reader for the input
    * @param systemId is the URI for the input
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(Reader reader, String systemId)
      throws DocumentException
   {
      InputSource source = new InputSource(reader);
      source.setSystemId(systemId);
      if(this.encoding != null)
      {
         source.setEncoding(this.encoding);
      }

      return read(source);
   }

   /**
    * <p> Reads a Document from the given <code>InputSource</code> using SAX
    * </p>
    * 
    * @param in <code>InputSource</code> to read from.
    * 
    * @return the newly created Document instance
    * 
    * @throws DocumentException if an error occurs during parsing.
    */
   public Document read(InputSource in) throws DocumentException
   {
      try
      {
         XMLReader reader = getXMLReader();

         reader = installXMLFilter(reader);

         EntityResolver thatEntityResolver = this.entityResolver;

         if(thatEntityResolver == null)
         {
            thatEntityResolver = createDefaultEntityResolver(in.getSystemId());
            this.entityResolver = thatEntityResolver;
         }

         reader.setEntityResolver(thatEntityResolver);

         SAXContentHandler contentHandler = createContentHandler(reader);
         contentHandler.setEntityResolver(thatEntityResolver);
         contentHandler.setInputSource(in);

         boolean internal = isIncludeInternalDTDDeclarations();
         boolean external = isIncludeExternalDTDDeclarations();

         contentHandler.setIncludeInternalDTDDeclarations(internal);
         contentHandler.setIncludeExternalDTDDeclarations(external);
         contentHandler.setMergeAdjacentText(isMergeAdjacentText());
         contentHandler.setStripWhitespaceText(isStripWhitespaceText());
         contentHandler.setIgnoreComments(isIgnoreComments());
         reader.setContentHandler(contentHandler);

         configureReader(reader, contentHandler);

         reader.parse(in);

         return contentHandler.getDocument();
      }
      catch(Exception e)
      {
         if(e instanceof SAXParseException)
         {
            // e.printStackTrace();
            SAXParseException parseException = (SAXParseException) e;
            String systemId = parseException.getSystemId();

            if(systemId == null)
            {
               systemId = "";
            }

            String message =
               "Error on line " + parseException.getLineNumber()
                  + " of document " + systemId + " : "
                  + parseException.getMessage();

            throw new DocumentException(message, e);
         }
         else
         {
            throw new DocumentException(e.getMessage(), e);
         }
      }
   }

   // Properties
   // -------------------------------------------------------------------------

   /**
    * DOCUMENT ME!
    * 
    * @return the validation mode, true if validating will be done otherwise
    *         false.
    */
   public boolean isValidating()
   {
      return validating;
   }

   /**
    * Sets the validation mode.
    * 
    * @param validation indicates whether or not validation should occur.
    */
   public void setValidation(boolean validation)
   {
      this.validating = validation;
   }

   /**
    * DOCUMENT ME!
    * 
    * @return whether internal DTD declarations should be expanded into the
    *         DocumentType object or not.
    */
   public boolean isIncludeInternalDTDDeclarations()
   {
      return includeInternalDTDDeclarations;
   }

   /**
    * Sets whether internal DTD declarations should be expanded into the
    * DocumentType object or not.
    * 
    * @param include whether or not DTD declarations should be expanded and
    *           included into the DocumentType object.
    */
   public void setIncludeInternalDTDDeclarations(boolean include)
   {
      this.includeInternalDTDDeclarations = include;
   }

   /**
    * DOCUMENT ME!
    * 
    * @return whether external DTD declarations should be expanded into the
    *         DocumentType object or not.
    */
   public boolean isIncludeExternalDTDDeclarations()
   {
      return includeExternalDTDDeclarations;
   }

   /**
    * Sets whether DTD external declarations should be expanded into the
    * DocumentType object or not.
    * 
    * @param include whether or not DTD declarations should be expanded and
    *           included into the DocumentType object.
    */
   public void setIncludeExternalDTDDeclarations(boolean include)
   {
      this.includeExternalDTDDeclarations = include;
   }

   /**
    * Sets whether String interning is enabled or disabled for element &
    * attribute names and namespace URIs. This proprety is enabled by default.
    * 
    * @return DOCUMENT ME!
    */
   public boolean isStringInternEnabled()
   {
      return stringInternEnabled;
   }

   /**
    * Sets whether String interning is enabled or disabled for element &
    * attribute names and namespace URIs
    * 
    * @param stringInternEnabled DOCUMENT ME!
    */
   public void setStringInternEnabled(boolean stringInternEnabled)
   {
      this.stringInternEnabled = stringInternEnabled;
   }

   /**
    * Returns whether adjacent text nodes should be merged together.
    * 
    * @return Value of property mergeAdjacentText.
    */
   public boolean isMergeAdjacentText()
   {
      return mergeAdjacentText;
   }

   /**
    * Sets whether or not adjacent text nodes should be merged together when
    * parsing.
    * 
    * @param mergeAdjacentText New value of property mergeAdjacentText.
    */
   public void setMergeAdjacentText(boolean mergeAdjacentText)
   {
      this.mergeAdjacentText = mergeAdjacentText;
   }

   /**
    * Sets whether whitespace between element start and end tags should be
    * ignored
    * 
    * @return Value of property stripWhitespaceText.
    */
   public boolean isStripWhitespaceText()
   {
      return stripWhitespaceText;
   }

   /**
    * Sets whether whitespace between element start and end tags should be
    * ignored.
    * 
    * @param stripWhitespaceText New value of property stripWhitespaceText.
    */
   public void setStripWhitespaceText(boolean stripWhitespaceText)
   {
      this.stripWhitespaceText = stripWhitespaceText;
   }

   /**
    * Returns whether we should ignore comments or not.
    * 
    * @return boolean
    */
   public boolean isIgnoreComments()
   {
      return ignoreComments;
   }

   /**
    * Sets whether we should ignore comments or not.
    * 
    * @param ignoreComments whether we should ignore comments or not.
    */
   public void setIgnoreComments(boolean ignoreComments)
   {
      this.ignoreComments = ignoreComments;
   }

   /**
    * DOCUMENT ME!
    * 
    * @return the <code>DocumentFactory</code> used to create document objects
    */
   public DocumentFactory getDocumentFactory()
   {
      if(factory == null)
      {
         factory = DocumentFactory.getInstance();
      }

      return factory;
   }

   /**
    * <p> This sets the <code>DocumentFactory</code> used to create new
    * documents. This method allows the building of custom DOM4J tree objects to
    * be implemented easily using a custom derivation of {@link DocumentFactory}
    * </p>
    * 
    * @param documentFactory <code>DocumentFactory</code> used to create DOM4J
    *           objects
    */
   public void setDocumentFactory(DocumentFactory documentFactory)
   {
      this.factory = documentFactory;
   }

   /**
    * DOCUMENT ME!
    * 
    * @return the <code>ErrorHandler</code> used by SAX
    */
   public ErrorHandler getErrorHandler()
   {
      return errorHandler;
   }

   /**
    * Sets the <code>ErrorHandler</code> used by the SAX
    * <code>XMLReader</code>.
    * 
    * @param errorHandler is the <code>ErrorHandler</code> used by SAX
    */
   public void setErrorHandler(ErrorHandler errorHandler)
   {
      this.errorHandler = errorHandler;
   }

   /**
    * Returns the current entity resolver used to resolve entities
    * 
    * @return DOCUMENT ME!
    */
   public EntityResolver getEntityResolver()
   {
      return entityResolver;
   }

   /**
    * Sets the entity resolver used to resolve entities.
    * 
    * @param entityResolver DOCUMENT ME!
    */
   public void setEntityResolver(EntityResolver entityResolver)
   {
      this.entityResolver = entityResolver;
   }

   /**
    * DOCUMENT ME!
    * 
    * @return the <code>XMLReader</code> used to parse SAX events
    * 
    * @throws SAXException DOCUMENT ME!
    */
   public XMLReader getXMLReader() throws SAXException
   {
      if(xmlReader == null)
      {
         xmlReader = createXMLReader();
      }

      return xmlReader;
   }

   /**
    * Sets the <code>XMLReader</code> used to parse SAX events
    * 
    * @param reader is the <code>XMLReader</code> to parse SAX events
    */
   public void setXMLReader(XMLReader reader)
   {
      this.xmlReader = reader;
   }

   /**
    * Returns encoding used for InputSource (null means system default encoding)
    * 
    * @return encoding used for InputSource
    * 
    */
   public String getEncoding()
   {
      return encoding;
   }

   /**
    * Sets encoding used for InputSource (null means system default encoding)
    * 
    * @param encoding is encoding used for InputSource
    */
   public void setEncoding(String encoding)
   {
      this.encoding = encoding;
   }

   /**
    * Sets the class name of the <code>XMLReader</code> to be used to parse
    * SAX events.
    * 
    * @param xmlReaderClassName is the class name of the <code>XMLReader</code>
    *           to parse SAX events
    * 
    * @throws SAXException DOCUMENT ME!
    */
   public void setXMLReaderClassName(String xmlReaderClassName)
      throws SAXException
   {
      setXMLReader(XMLReaderFactory.createXMLReader(xmlReaderClassName));
   }

   /**
    * Adds the <code>ElementHandler</code> to be called when the specified
    * path is encounted.
    * 
    * @param path is the path to be handled
    * @param handler is the <code>ElementHandler</code> to be called by the
    *           event based processor.
    */
   public void addHandler(String path, ElementHandler handler)
   {
      getDispatchHandler().addHandler(path, handler);
   }

   /**
    * Removes the <code>ElementHandler</code> from the event based processor,
    * for the specified path.
    * 
    * @param path is the path to remove the <code>ElementHandler</code> for.
    */
   public void removeHandler(String path)
   {
      getDispatchHandler().removeHandler(path);
   }

   /**
    * When multiple <code>ElementHandler</code> instances have been
    * registered, this will set a default <code>ElementHandler</code> to be
    * called for any path which does <b>NOT </b> have a handler registered.
    * 
    * @param handler is the <code>ElementHandler</code> to be called by the
    *           event based processor.
    */
   public void setDefaultHandler(ElementHandler handler)
   {
      getDispatchHandler().setDefaultHandler(handler);
   }

   /**
    * This method clears out all the existing handlers and default handler
    * setting things back as if no handler existed. Useful when reusing an
    * object instance.
    */
   public void resetHandlers()
   {
      getDispatchHandler().resetHandlers();
   }

   /**
    * Returns the SAX filter being used to filter SAX events.
    * 
    * @return the SAX filter being used or null if no SAX filter is installed
    */
   public XMLFilter getXMLFilter()
   {
      return xmlFilter;
   }

   /**
    * Sets the SAX filter to be used when filtering SAX events
    * 
    * @param filter is the SAX filter to use or null to disable filtering
    */
   public void setXMLFilter(XMLFilter filter)
   {
      this.xmlFilter = filter;
   }

   // Implementation methods
   // -------------------------------------------------------------------------

   /**
    * Installs any XMLFilter objects required to allow the SAX event stream to
    * be filtered and preprocessed before it gets to dom4j.
    * 
    * @param reader DOCUMENT ME!
    * 
    * @return the new XMLFilter if applicable or the original XMLReader if no
    *         filter is being used.
    */
   protected XMLReader installXMLFilter(XMLReader reader)
   {
      XMLFilter filter = getXMLFilter();

      if(filter != null)
      {
         // find the root XMLFilter
         XMLFilter root = filter;

         while(true)
         {
            XMLReader parent = root.getParent();

            if(parent instanceof XMLFilter)
            {
               root = (XMLFilter) parent;
            }
            else
            {
               break;
            }
         }

         root.setParent(reader);

         return filter;
      }

      return reader;
   }

   protected DispatchHandler getDispatchHandler()
   {
      if(dispatchHandler == null)
      {
         dispatchHandler = new DispatchHandler();
      }

      return dispatchHandler;
   }

   protected void setDispatchHandler(DispatchHandler dispatchHandler)
   {
      this.dispatchHandler = dispatchHandler;
   }

   /**
    * Factory Method to allow alternate methods of creating and configuring
    * XMLReader objects
    * 
    * @return DOCUMENT ME!
    * 
    * @throws SAXException DOCUMENT ME!
    */
   protected XMLReader createXMLReader() throws SAXException
   {
      return SAXHelper.createXMLReader(isValidating());
   }

   /**
    * Configures the XMLReader before use
    * 
    * @param reader DOCUMENT ME!
    * @param handler DOCUMENT ME!
    * 
    * @throws DocumentException DOCUMENT ME!
    */
   protected void configureReader(XMLReader reader, DefaultHandler handler)
      throws DocumentException
   {
      // configure lexical handling
      SAXHelper.setParserProperty(reader, SAX_LEXICALHANDLER, handler);

      // try alternate property just in case
      SAXHelper.setParserProperty(reader, SAX_LEXICAL_HANDLER, handler);

      // register the DeclHandler
      if(includeInternalDTDDeclarations || includeExternalDTDDeclarations)
      {
         SAXHelper.setParserProperty(reader, SAX_DECL_HANDLER, handler);
      }

      // configure namespace support
      SAXHelper.setParserFeature(reader, SAX_NAMESPACES, true);

      SAXHelper.setParserFeature(reader, SAX_NAMESPACE_PREFIXES, false);

      // string interning
      SAXHelper.setParserFeature(reader, SAX_STRING_INTERNING,
         isStringInternEnabled());

      // external entites
      /*
       * SAXHelper.setParserFeature( reader,
       * "http://xml.org/sax/properties/external-general-entities",
       * includeExternalGeneralEntities ); SAXHelper.setParserFeature( reader,
       * "http://xml.org/sax/properties/external-parameter-entities",
       * includeExternalParameterEntities );
       */
      // use Locator2 if possible
      SAXHelper.setParserFeature(reader,
         "http://xml.org/sax/features/use-locator2", true);

      try
      {
         // configure validation support
         reader.setFeature("http://xml.org/sax/features/validation",
            isValidating());

         if(errorHandler != null)
         {
            reader.setErrorHandler(errorHandler);
         }
         else
         {
            reader.setErrorHandler(handler);
         }
      }
      catch(Exception e)
      {
         if(isValidating())
         {
            throw new DocumentException("Validation not supported for"
               + " XMLReader: " + reader, e);
         }
      }
   }

   /**
    * Factory Method to allow user derived SAXContentHandler objects to be used
    * 
    * @param reader DOCUMENT ME!
    * 
    * @return DOCUMENT ME!
    */
   protected SAXContentHandler createContentHandler(XMLReader reader)
   {
      return new SAXContentHandler(getDocumentFactory(), dispatchHandler);
   }

   protected EntityResolver createDefaultEntityResolver(String systemId)
   {
      String prefix = null;

      if((systemId != null) && (systemId.length() > 0))
      {
         int idx = systemId.lastIndexOf('/');

         if(idx > 0)
         {
            prefix = systemId.substring(0, idx + 1);
         }
      }

      return new SAXEntityResolver(prefix);
   }

   protected static class SAXEntityResolver implements EntityResolver,
      Serializable
   {
      protected String uriPrefix;

      public SAXEntityResolver(String uriPrefix)
      {
         this.uriPrefix = uriPrefix;
      }

      public InputSource resolveEntity(String publicId, String systemId)
      {
         // try create a relative URI reader...
         if((systemId != null) && (systemId.length() > 0))
         {
            if((uriPrefix != null) && (systemId.indexOf(':') <= 0))
            {
               systemId = uriPrefix + systemId;
            }
         }

         return new InputSource(systemId);
      }
   }
}

/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived
 * from this Software without prior written permission of MetaStuff, Ltd. For
 * written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may
 * "DOM4J" appear in their names without prior written permission of MetaStuff,
 * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
